import os
import traceback
from datetime import timedelta

import joblib
import lightgbm as lgb
import numpy as np
import pandas as pd
from matplotlib import pyplot as plt
from sklearn.ensemble import RandomForestRegressor, VotingRegressor
from sklearn.impute import SimpleImputer
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
from sklearn.multioutput import MultiOutputRegressor
from sklearn.neural_network import MLPRegressor
from sklearn.preprocessing import StandardScaler, LabelEncoder
from sqlalchemy import create_engine  # 替换pymysql为SQLAlchemy

# 设置支持中文的字体
plt.rcParams['font.sans-serif'] = ['Microsoft YaHei']
plt.rcParams['axes.unicode_minus'] = False


class TemperatureForecaster:
    def __init__(self, target_col):
        """
        初始化温度预测器

        参数:
            target_col (str): 目标温度列名
        """
        self.model_name = 'MultiDayEnsembleModel'
        self.target_col = target_col.lower()
        lags = [1, 2, 3, 4, 5, 6, 7, 14, 21, 30, 60, 90, 120, 150, 180]
        self.lags = sorted(lags)
        self.windows = [3, 7, 14, 30]
        self.initial_data_size = 365  # 需要的历史数据天数
        self.model_dir = os.path.join('app', 'saved_models', self.model_name)
        os.makedirs(self.model_dir, exist_ok=True)

    def load_data_from_database(self, table_name='weather_data'):
        """
        从数据库加载天气数据

        参数:
            table_name (str): 数据库表名

        返回:
            pd.DataFrame: 加载的数据
        """
        # 使用SQLAlchemy创建连接
        db_url = "mysql+pymysql://root:1026@localhost/guilin_temp?charset=utf8mb4"
        engine = create_engine(db_url)

        try:
            # 使用SQLAlchemy引擎读取数据
            df = pd.read_sql_table(table_name, engine)

            # 处理日期和索引
            df['record_date'] = pd.to_datetime(df['record_date'])
            df.set_index('record_date', inplace=True)
            df = df.sort_index()

            print(f'数据库数据加载成功，前5行数据为:\n{df.head()}')

            # 将所有列名转换为小写
            df.columns = df.columns.str.lower()

            # 处理缺失值 - 使用插值法
            if self.target_col in df.columns:
                df[self.target_col] = df[self.target_col].interpolate(method='time')
            else:
                raise ValueError(f"目标列 {self.target_col} 不存在于数据中")

            # 处理分类变量
            if 'season' in df.columns:
                df['season'] = df['season'].astype('category')
                df['season_code'] = df['season'].cat.codes

            return df  # 显式返回 DataFrame

        except Exception as e:
            raise ValueError(f"读取数据库数据时出错: {e}")

        finally:
            engine.dispose()

    def create_features(self, df):
        """创建预测所需的特征"""
        df = df.copy()

        # 创建滞后特征
        for lag in self.lags:
            df[f'{self.target_col}_lag{lag}'] = df[self.target_col].shift(lag)

        # 创建移动平均特征
        for window in self.windows:
            df[f'{self.target_col}_ma{window}'] = df[self.target_col].rolling(window=window).mean()

        # 创建差分特征
        df[f'{self.target_col}_diff1'] = df[self.target_col].diff()
        df[f'{self.target_col}_diff7'] = df[self.target_col].diff(7)
        df[f'{self.target_col}_diff30'] = df[self.target_col].diff(30)

        # 创建季节性特征
        df['day_of_year'] = df.index.dayofyear
        df['month'] = df.index.month
        df['day_of_month'] = df.index.day
        df['week_of_year'] = df.index.isocalendar().week
        df['season_sin'] = np.sin(2 * np.pi * df.index.dayofyear / 365.25)
        df['season_cos'] = np.cos(2 * np.pi * df.index.dayofyear / 365.25)

        return df

    def load_model(self, day):
        """
        加载指定day的模型

        参数:
            day (int): 要预测的天数(1到最大可用天数)

        返回:
            dict: 包含模型和特征的字典

        异常:
            ValueError: 如果找不到模型文件
        """
        filename = f'{self.target_col}_day_{day}.pkl'
        model_path = os.path.join(self.model_dir, filename)

        if not os.path.exists(model_path):
            raise ValueError(f"找不到模型文件: {model_path}")

        model_info = joblib.load(model_path)

        # 将特征名称转换为小写
        model_info['features'] = [feature.lower() for feature in model_info['features']]

        return model_info

    def _predict_day(self, initial_data, day, model_info):
        """预测指定day的温度"""
        current_data = initial_data.copy()

        # 为初始数据创建特征
        current_data = self.create_features(current_data)

        # 选择模型训练时使用的特征
        model_features = model_info['features']

        # 检查并处理特征不匹配问题
        current_features = current_data.columns.tolist()
        missing_features = [f for f in model_features if f not in current_features]
        if missing_features:
            for feature in missing_features:
                current_data[feature] = 0

        # 选择模型所需的特征
        X = current_data[model_features].iloc[-self.initial_data_size:].copy()

        # 处理分类特征'season'
        if 'season' in X.columns:
            le = LabelEncoder()
            X['season'] = le.fit_transform(X['season'])

        # 进行缺失值填充
        imputer = SimpleImputer(strategy='mean')
        X = imputer.fit_transform(X)

        # 进行标准化
        scaler = StandardScaler()
        X = scaler.fit_transform(X)

        # 预测
        pred = model_info['model'].predict(X)[-1]  # 取最后一个预测值
        return pred

    def _save_prediction_plot(self, history_data, predictions, horizon):
        """
        保存预测结果图，显示预测日期之前一年的完整数据

        参数:
            history_data (pd.Series): 历史温度数据
            predictions (dict): 预测结果 {日期: 温度}
            horizon (int): 预测天数
        """
        # 创建绘图目录
        plot_dir = os.path.join('app', 'static', 'plots')
        os.makedirs(plot_dir, exist_ok=True)

        # 准备数据
        forecast_start_date = pd.to_datetime(list(predictions.keys())[0])
        one_year_before = forecast_start_date - pd.DateOffset(years=1)

        # 筛选出预测开始日期前一年的数据
        full_year_history = history_data[history_data.index >= one_year_before]

        history_dates = full_year_history.index
        history_values = full_year_history.values
        forecast_dates = pd.to_datetime(list(predictions.keys()))
        forecast_values = list(predictions.values())

        # 创建图表
        plt.figure(figsize=(15, 7))

        # 绘制历史数据（一整年）
        plt.plot(history_dates, history_values,
                 label='历史温度', color='blue', linewidth=2)

        # 绘制预测数据
        plt.plot(forecast_dates, forecast_values,
                 'ro-', label=f'未来{horizon}天预测', markersize=6, linewidth=2)

        # 添加标题和标签
        if self.target_col == 'guilin_max':
            title_name = '桂林市最高气温'
        else:
            title_name = '桂林市最低气温'
        plt.title(f'{title_name} 温度趋势 (1年历史数据 + {horizon}天预测)', fontsize=14)
        plt.xlabel('日期', fontsize=12)
        plt.ylabel('温度 (°C)', fontsize=12)

        # 添加图例和网格
        plt.legend(fontsize=12, loc='upper right')
        plt.grid(True, linestyle='--', alpha=0.7)

        # 设置x轴范围，确保显示完整一年加预测天数
        end_date = forecast_dates[-1] + pd.DateOffset(days=1)
        plt.xlim([one_year_before, end_date])

        # 旋转日期标签
        plt.xticks(rotation=45)

        # 自动调整布局
        plt.tight_layout()

        # 保存图像
        filename = f'{title_name}_预测_{horizon}天.png'
        plot_path = os.path.join(plot_dir, filename)
        plt.savefig(plot_path, dpi=300, bbox_inches='tight')
        plt.close()

        print(f"预测图已保存到: {plot_path}")

    def predict(self, horizon=7, data=None, table_name='weather_data'):
        """
        预测未来温度

        参数:
            horizon (int): 要预测的天数
            data (pd.DataFrame): 可选，如果提供则使用此数据，否则从数据库加载
            table_name (str): 数据库表名

        返回:
            dict: 包含预测结果的字典，格式为 {'日期': '预测温度'}

        异常:
            ValueError: 如果数据不足或预测天数超出范围
        """
        # 验证预测天数
        if not isinstance(horizon, int) or horizon <= 0:
            raise ValueError("预测天数必须是正整数")

        # 加载数据
        if data is None:
            data = self.load_data_from_database(table_name)

        if len(data) < self.initial_data_size:
            raise ValueError(f"需要至少 {self.initial_data_size} 天的历史数据来进行预测")

        # 准备数据
        my_df = self.create_features(data.copy())
        initial_data = my_df[-self.initial_data_size * 3:].copy()
        predictions = {}

        # 逐天预测
        for day in range(1, horizon + 1):
            try:
                model_info = self.load_model(day)
                day_pred = self._predict_day(initial_data, day, model_info)
                predictions[day] = round(day_pred, 1)  # 保留一位小数
            except ValueError as e:
                # 如果找不到某天的模型，停止预测
                if day == 1:
                    raise ValueError(f"无法预测: {str(e)}")
                break  # 停止预测，返回已预测的部分结果

        # 生成预测日期
        last_date = initial_data.index[-1]
        forecast_dates = [last_date + timedelta(days=day) for day in range(1, len(predictions) + 1)]

        # 格式化结果并保留一位小数
        result = {
            str(date.date()): float(f"{predictions[day]:.1f}")
            for day, date in zip(range(1, len(predictions) + 1), forecast_dates)
        }

        # 保存预测结果图
        self._save_prediction_plot(
            history_data=initial_data[self.target_col][-365:],  # 使用最近60天的历史数据
            predictions=result,
            horizon=horizon
        )

        # 修改返回结构，简化嵌套并确保predicted_days准确
        return {
            'predictions': result
        }

    def evaluate_multi_model(self, test_days=90, table_name='weather_data'):
        """
        多模型前瞻性评估（优化版：先创建特征再分割数据）
        参数:
            test_days (int): 预测未来多少天（需<=最大模型天数）
            table_name (str): 数据表名
        返回:
            dict: 包含预测值和评估指标
        """
        try:
            print(f"\n开始多模型前瞻性评估 (测试天数={test_days})...")

            # 1. 数据准备（一次性创建所有特征）
            full_data = self.load_data_from_database(table_name)
            if len(full_data) < test_days + 1:
                raise ValueError(f"需要至少 {test_days + 1} 天数据（基准日+测试天数）")

            # 提前创建所有特征（包含滞后特征等）
            featured_data = self.create_features(full_data.copy())

            # 2. 分割数据（保持特征完整性）
            base_day = featured_data.iloc[-test_days - 1:-test_days]  # 基准日（带特征）
            future_days = full_data[-test_days:].copy()  # 未来真实值（原始数据）
            y_true = future_days[self.target_col].values

            # 3. 多模型预测
            predictions = []
            for day in range(1, test_days + 1):
                model_info = self.load_model(day)

                # 直接从已创建的特征中选择所需特征
                X = base_day[model_info['features']].copy()

                # 分类特征编码
                if 'season' in X.columns:
                    le = LabelEncoder()
                    X['season'] = le.fit_transform(X['season'])

                # 预处理 & 预测
                X_processed = model_info['imputer'].transform(X)
                X_scaled = model_info['scaler'].transform(X_processed)
                pred = model_info['model'].predict(X_scaled)[0]
                predictions.append(pred)

            # 4. 计算指标（保留4位小数）
            metrics = {
                'MAE': round(mean_absolute_error(y_true, predictions), 4),
                'RMSE': round(np.sqrt(mean_squared_error(y_true, predictions)), 4),
                'R2': round(r2_score(y_true, predictions), 4),
                'test_days': test_days,
                'base_date': str(base_day.index[0])[:10],
                'model_range': f'day1-day{test_days}'
            }

            # 5. 打印评估结果
            print("\n多模型评估结果:")
            print(f"基准日: {metrics['base_date']}")
            print(f"使用模型: {metrics['model_range']}")
            for k, v in metrics.items():
                if k not in ['test_days', 'base_date', 'model_range']:
                    print(f"{k}: {v:.4f}")

            # 6. 生成结果图表
            plot_path = self._save_evaluation_plot(
                dates=future_days.index,
                actual=y_true,
                predicted=predictions,
                title=f'Ensemble多模型前瞻预测_{test_days}天_评估图',
                metrics=metrics
            )

            metrics = {
                'MAE': round(mean_absolute_error(y_true, predictions), 4),
                'RMSE': round(np.sqrt(mean_squared_error(y_true, predictions)), 4),
                'R2': round(r2_score(y_true, predictions), 4),
            }

            return {
                'status': 'success',
                'metrics': metrics,
                'plot_path': plot_path,
                'predictions': list(zip(range(1, test_days + 1), predictions, y_true))  # 添加详细预测结果
            }

        except Exception as e:
            print(f"评估错误: {traceback.format_exc()}")
            return {
                'status': 'error',
                'message': str(e)
            }

    def evaluate_single_model(self, test_days=30, table_name='weather_data'):
        """
        在验证集上使用day=1模型进行逐步预测评估（优化版）
        参数:
            test_days (int): 验证集天数
            table_name (str): 数据表名
        返回:
            dict: 包含逐步预测的评估结果
        """
        try:
            print(f"\n开始在验证集上进行单模型逐步评估 (day=1)...")

            # 1. 数据准备（一次性创建所有特征）
            data = self.load_data_from_database(table_name)
            if len(data) < test_days + 1:
                raise ValueError(f"需要至少 {test_days + 1} 天数据（1天基准日+{test_days}天验证集）")

            # 提前创建所有特征
            full_data = self.create_features(data.copy())

            # 2. 分割数据（最后test_days天作为验证集）
            val_start = -test_days - 1
            val_data = full_data.iloc[val_start:-1]  # 验证基准数据（已包含特征）
            val_y = full_data.iloc[-test_days:][self.target_col].values  # 验证集真实值

            # 3. 加载day=1模型
            model_info = self.load_model(day=1)

            # 4. 逐步预测
            predictions = []
            for i in range(test_days):
                # 取当前预测基准日（滚动窗口）
                current_features = val_data.iloc[i:i + 1][model_info['features']].copy()

                # 处理分类特征
                if 'season' in current_features.columns:
                    le = LabelEncoder()
                    current_features['season'] = le.fit_transform(current_features['season'])

                # 预处理 & 预测
                X_processed = model_info['imputer'].transform(current_features)
                X_scaled = model_info['scaler'].transform(X_processed)
                pred = model_info['model'].predict(X_scaled)[0]
                predictions.append(pred)

            # 5. 计算指标
            metrics = {
                'MAE': round(mean_absolute_error(val_y, predictions), 4),
                'RMSE': round(np.sqrt(mean_squared_error(val_y, predictions)), 4),
                'R2': round(r2_score(val_y, predictions), 4),
                'test_days': test_days,
                'first_date': str(val_data.index[0])[:10],
                'last_date': str(val_data.index[-1])[:10],
                'model_used': 'day=1 (逐步预测)'
            }

            # 6. 打印评估结果
            print("\n单模型逐步评估结果:")
            print(f"验证集日期范围: {metrics['first_date']} 至 {metrics['last_date']}")
            for k, v in metrics.items():
                if k not in ['test_days', 'first_date', 'last_date', 'model_used']:
                    print(f"{k}: {v:.4f}")

            # 7. 生成评估图表
            plot_path = self._save_evaluation_plot(
                dates=full_data.index[-test_days:],
                actual=val_y,
                predicted=predictions,
                title=f'Ensemble模型(Day=1)_{test_days}天验证集_评估图',
                metrics=metrics
            )

            new_metrics = {
                'MAE': metrics['MAE'],
                'RMSE': metrics['RMSE'],
                'R2': metrics['R2'],
            }

            return {
                'status': 'success',
                'metrics': new_metrics,
                'plot_path': plot_path,
            }

        except Exception as e:
            print(f"评估错误: {traceback.format_exc()}")
            return {
                'status': 'error',
                'message': str(e)
            }

    def _save_evaluation_plot(self, dates, actual, predicted, title, metrics):

        plt.figure(figsize=(15, 7))

        # 绘制实际值和预测值曲线
        plt.plot(dates, actual, 'b-', label='实际温度', linewidth=2)
        plt.plot(dates, predicted, 'r--', label='预测温度', linewidth=2)

        # 标记最大误差点（保留2位小数）
        errors = np.abs(actual - predicted)
        max_error_idx = np.argmax(errors)
        max_error = round(errors[max_error_idx], 2)
        plt.scatter(dates[max_error_idx], actual[max_error_idx],
                    color='green', s=100, label=f'最大误差点: {max_error}°C')

        # 添加评估指标文本（使用传入的已格式化指标）
        metrics_text = (
            f"MAE: {metrics['MAE']}\n"
            f"RMSE: {metrics['RMSE']}\n"
            f"R²: {metrics['R2']}"
        )
        plt.text(0.02, 0.95, metrics_text, transform=plt.gca().transAxes,
                 bbox=dict(facecolor='white', alpha=0.8))

        # 图表装饰
        plt.title(title, fontsize=14)
        plt.xlabel('日期', fontsize=12)
        plt.ylabel('温度 (°C)', fontsize=12)
        plt.legend(fontsize=12, loc='upper right')
        plt.grid(True, linestyle='--', alpha=0.5)
        plt.xticks(rotation=45)
        plt.tight_layout()

        # 保存图像
        plot_dir = os.path.join('app', 'static', 'evaluation_plots')
        os.makedirs(plot_dir, exist_ok=True)
        filename = f'{self.target_col}_{title}.png'
        save_path = os.path.join(plot_dir, filename)
        plt.savefig(save_path, dpi=300, bbox_inches='tight')
        plt.close()

        print(f"评估图表已保存到: {save_path}")
        return save_path


    def train_models(self, horizon=30, data=None, table_name='weather_data'):
        """
        训练多天预测模型

        参数:
            horizon (int): 要训练的最大预测天数
            data (pd.DataFrame): 可选，如果提供则使用此数据，否则从数据库加载
            table_name (str): 数据库表名

        返回:
            dict: 包含训练结果的字典
        """
        try:
            # 加载数据
            if data is None:
                data = self.load_data_from_database(table_name)

            # 检查数据是否足够
            if len(data) < self.initial_data_size * 2:
                raise ValueError(f"需要至少 {self.initial_data_size * 2} 天的历史数据来进行训练")

            df = self.create_features(data.copy())

            # 处理异常值 - 使用分位数方法
            q_low = df[self.target_col].quantile(0.01)
            q_hi = df[self.target_col].quantile(0.99)
            df[self.target_col] = df[self.target_col].clip(q_low, q_hi)

            # 处理分类变量
            if 'season' in df.columns:
                le = LabelEncoder()
                df['season'] = le.fit_transform(df['season'])

            # 划分训练集和验证集
            train_df = df.iloc[:-self.initial_data_size].copy()

            # 训练每个day的模型
            for day in range(1, horizon + 1):
                print(f"\n训练第{day}天的预测模型...")

                # 准备训练数据
                X, y, feature_cols = self._prepare_training_data(train_df, day)

                # 划分训练集和验证集 (80%训练, 20%验证)
                X_train, X_val, y_train, y_val = train_test_split(
                    X, y, test_size=0.2, shuffle=False
                )

                # 使用 SimpleImputer 填充 NaN 值
                imputer = SimpleImputer(strategy='mean')
                X_train = imputer.fit_transform(X_train)
                X_val = imputer.transform(X_val)

                # 数据标准化
                scaler = StandardScaler()
                X_train = scaler.fit_transform(X_train)
                X_val = scaler.transform(X_val)

                # 定义各个基模型
                # 1. 线性回归
                lr = LinearRegression()
                lr.fit(X_train, y_train)

                # 2. 随机森林
                rf = RandomForestRegressor(
                    n_estimators=100,
                    random_state=42,
                    n_jobs=-1
                )
                rf.fit(X_train, y_train)

                # 3. MLP
                mlp = MLPRegressor(
                    hidden_layer_sizes=(100, 50),
                    activation='relu',
                    solver='adam',
                    alpha=0.0001,
                    batch_size='auto',
                    learning_rate='adaptive',
                    learning_rate_init=0.001,
                    max_iter=500,
                    early_stopping=True,
                    validation_fraction=0.1,
                    random_state=42,
                    verbose=False
                )
                mlp.fit(X_train, y_train)

                # 4. LightGBM
                lgbm = lgb.LGBMRegressor(
                    objective='regression',
                    num_leaves=127,
                    learning_rate=0.01,
                    n_estimators=1800,
                    max_depth=9,
                    min_child_samples=5,
                    subsample=0.8,
                    colsample_bytree=0.8,
                    random_state=42,
                    verbosity=-1
                )
                lgbm.fit(X_train, y_train)

                # 创建集成模型 - 使用投票回归器
                ensemble = VotingRegressor(
                    estimators=[
                        ('lr', lr),
                        ('rf', rf),
                        ('mlp', mlp),
                        ('lgbm', lgbm)
                    ],
                    n_jobs=-1
                )

                # 训练集成模型
                print(f"训练第{day}天的集成模型中...")
                ensemble.fit(X_train, y_train)

                # 评估模型在验证集上的表现
                val_pred = ensemble.predict(X_val)
                mae = mean_absolute_error(y_val, val_pred)
                r2 = r2_score(y_val, val_pred)
                # 计算RMSE
                mse = mean_squared_error(y_val, val_pred)
                rmse = np.sqrt(mse)  # 假设已导入numpy为np

                print(f"第{day}天模型验证集表现: MAE = {mae:.4f}, RMSE = {rmse:.4f},R² = {r2:.4f}")

                # 准备模型信息
                model_info = {
                    'day': day,
                    'model': ensemble,
                    'features': feature_cols,
                    'imputer': imputer,
                    'scaler': scaler
                }

                # 保存模型
                self._save_single_model(model_info, day)

                # 清除当前模型的变量以释放内存
                del lr, rf, mlp, lgbm, ensemble, model_info
                import gc
                gc.collect()

            print(f"\n已完成1-{horizon}天的所有预测模型训练")

            return {
                'status': 'success',
                'message': f'成功训练1-{horizon}天的预测模型',
                'horizon': horizon
            }

        except Exception as e:
            print(f"训练错误: {traceback.format_exc()}")
            return {
                'status': 'error',
                'message': str(e)
            }

    def _prepare_training_data(self, df, day):
        """准备第day天的训练数据"""
        df = self.create_features(df)

        # 定义特征列
        feature_cols = [
            col for col in df.columns
            if col != self.target_col
               and col != 'record_date'
               and col != 'year'
               and col != 'id'
        ]

        # 创建目标变量 (day天后的温度)
        df[f'target_day{day}'] = df[self.target_col].shift(-day)
        df = df.dropna(subset=[f'target_day{day}'])

        return df[feature_cols], df[f'target_day{day}'], feature_cols

    def _save_single_model(self, model_info, day):
        """保存单个模型到文件"""
        model_save_dir = os.path.join(self.model_dir, 'next_train_ensemble')
        os.makedirs(model_save_dir, exist_ok=True)

        filename = os.path.join(model_save_dir, f'{self.target_col}_day_{day}.pkl')
        joblib.dump(model_info, filename)
        print(f"第{day}天模型已保存到 {filename}")

    def train_multioutput_model(self, horizon=90, data=None, table_name='weather_data'):
        """
        使用MultiOutputRegressor训练多输出模型（同时预测多天温度）

        参数:
            horizon (int): 要预测的最大天数
            data (pd.DataFrame): 可选，如果提供则使用此数据，否则从数据库加载
            table_name (str): 数据库表名

        返回:
            dict: 包含训练结果的字典
        """
        try:
            # 加载数据
            if data is None:
                data = self.load_data_from_database(table_name)

            # 检查数据是否足够
            if len(data) < self.initial_data_size * 2:
                raise ValueError(f"需要至少 {self.initial_data_size * 2} 天的历史数据来进行训练")

            # 创建特征
            df = self.create_features(data.copy())

            # 处理异常值
            q_low = df[self.target_col].quantile(0.01)
            q_hi = df[self.target_col].quantile(0.99)
            df[self.target_col] = df[self.target_col].clip(q_low, q_hi)

            # 处理分类变量 - 保存编码器
            if 'season' in df.columns:
                self.season_encoder = LabelEncoder()
                df['season'] = self.season_encoder.fit_transform(df['season'])
                # 保存编码器
                joblib.dump(self.season_encoder,
                            os.path.join(self.model_dir, 'season_encoder.pkl'))

            # 准备多输出目标变量 (horizon天的温度)
            y = pd.DataFrame()
            for day in range(1, horizon + 1):
                y[f'day_{day}'] = df[self.target_col].shift(-day)

            # 移除包含NaN的行
            df = df.iloc[:-horizon]
            y = y.dropna()

            # 定义特征列
            feature_cols = [
                col for col in df.columns
                if col != self.target_col
                   and col != 'record_date'
                   and col != 'year'
                   and col != 'id'
            ]
            X = df[feature_cols]

            # 划分训练集和验证集 (80%训练, 20%验证)
            X_train, X_val, y_train, y_val = train_test_split(
                X, y, test_size=0.2, shuffle=False
            )

            # 数据预处理
            imputer = SimpleImputer(strategy='mean')
            X_train = imputer.fit_transform(X_train)
            X_val = imputer.transform(X_val)

            scaler = StandardScaler()
            X_train = scaler.fit_transform(X_train)
            X_val = scaler.transform(X_val)

            # 创建基模型 (这里使用LightGBM作为示例)
            base_model = lgb.LGBMRegressor(
                objective='regression',
                num_leaves=63,
                learning_rate=0.05,
                n_estimators=1000,
                random_state=42,
                verbosity=-1
            )

            # 创建MultiOutputRegressor
            multi_model = MultiOutputRegressor(
                estimator=base_model,
                n_jobs=-1  # 并行训练所有输出
            )

            # 训练模型
            print("训练MultiOutputRegressor模型中...")
            multi_model.fit(X_train, y_train)

            # 评估模型
            val_pred = multi_model.predict(X_val)
            metrics = {
                'MAE': round(mean_absolute_error(y_val, val_pred), 4),
                'RMSE': round(np.sqrt(mean_squared_error(y_val, val_pred)), 4),
                'R2': round(r2_score(y_val, val_pred), 4)
            }

            # 保存模型
            model_info = {
                'model': multi_model,
                'features': feature_cols,
                'imputer': imputer,
                'scaler': scaler,
                'horizon': horizon
            }

            filename = os.path.join(self.model_dir, f'{self.target_col}_multioutput.pkl')
            joblib.dump(model_info, filename)
            print(f"MultiOutput模型已保存到 {filename}")

            return {
                'status': 'success',
                'message': f'成功训练MultiOutput模型(1-{horizon}天)',
                'metrics': metrics,
                'model_path': filename
            }

        except Exception as e:
            print(f"MultiOutput训练错误: {traceback.format_exc()}")
            return {
                'status': 'error',
                'message': str(e)
            }

    def predict_multioutput(self, horizon=90, data=None, table_name='weather_data'):
        """
        使用MultiOutputRegressor模型进行多步预测

        参数:
            horizon (int): 要预测的天数
            data (pd.DataFrame): 可选，如果提供则使用此数据
            table_name (str): 数据库表名

        返回:
            dict: 包含预测结果的字典 {'日期': '预测温度'}
        """
        try:
            # 加载模型
            model_path = os.path.join(self.model_dir, f'{self.target_col}_multioutput.pkl')
            if not os.path.exists(model_path):
                raise ValueError("找不到MultiOutput模型，请先训练模型")

            model_info = joblib.load(model_path)

            # 加载季节编码器
            encoder_path = os.path.join(self.model_dir, 'season_encoder.pkl')
            if os.path.exists(encoder_path):
                self.season_encoder = joblib.load(encoder_path)

            # 加载数据
            if data is None:
                data = self.load_data_from_database(table_name)

            # 创建特征
            current_data = self.create_features(data.copy())
            current_data = current_data.iloc[-self.initial_data_size:].copy()

            # 处理分类变量
            if 'season' in current_data.columns and hasattr(self, 'season_encoder'):
                try:
                    current_data['season'] = self.season_encoder.transform(current_data['season'])
                except ValueError:
                    # 处理新出现的类别
                    current_data['season'] = -1  # 用-1标记未知类别

            # 准备输入特征
            X = current_data[model_info['features']]

            # 预处理
            X = model_info['imputer'].transform(X)
            X = model_info['scaler'].transform(X)

            # 预测
            predictions = model_info['model'].predict(X)[-1][:horizon]  # 取最后一个样本的预测

            # 生成预测日期
            last_date = current_data.index[-1]
            forecast_dates = [last_date + timedelta(days=day) for day in range(1, horizon + 1)]

            # 格式化结果
            result = {
                str(date.date()): round(temp, 1)
                for date, temp in zip(forecast_dates, predictions)
            }

            # 保存预测图
            self._save_prediction_plot(
                history_data=current_data[self.target_col][-365:],
                predictions=result,
                horizon=horizon
            )

            return {
                'predictions': result,
                'model_type': 'MultiOutputRegressor'
            }

        except Exception as e:
            print(f"MultiOutput预测错误: {traceback.format_exc()}")
            return {
                'status': 'error',
                'message': str(e)
            }

    def evaluate_multioutput_model(self, test_days=30, table_name='weather_data'):
        """
        评估多输出模型在数据集最后test_days天的表现（简化版）

        参数:
            test_days (int): 测试集天数
            table_name (str): 数据表名

        返回:
            dict: 包含评估指标和图表路径的字典
        """
        try:
            print(f"\n开始评估多输出模型 (测试天数={test_days})...")

            # 1. 加载模型
            model_path = os.path.join(self.model_dir, f'{self.target_col}_multioutput.pkl')
            if not os.path.exists(model_path):
                raise ValueError("找不到MultiOutput模型，请先训练模型")

            model_info = joblib.load(model_path)
            horizon = model_info['horizon']

            if test_days > horizon:
                raise ValueError(f"测试天数({test_days})超过模型训练的预测范围({horizon}天)")

            # 2. 加载季节编码器（如果存在）
            encoder_path = os.path.join(self.model_dir, 'season_encoder.pkl')
            if os.path.exists(encoder_path):
                self.season_encoder = joblib.load(encoder_path)

            # 3. 加载完整数据
            full_data = self.load_data_from_database(table_name)
            if len(full_data) < test_days + self.initial_data_size:
                raise ValueError(f"需要至少 {test_days + self.initial_data_size} 天的历史数据来进行评估")

            # 4. 设置基准日期（倒数第test_days+1天）
            base_date_idx = len(full_data) - test_days - 1
            base_date = full_data.index[base_date_idx]
            print(f"基准日期: {base_date.date()}")

            # 5. 准备基准日期前的历史数据（用于特征生成）
            history_data = full_data.iloc[base_date_idx - self.initial_data_size: base_date_idx + 1].copy()

            # 6. 使用预测函数预测未来test_days天的温度
            predictions = self.predict_multioutput(
                horizon=test_days,
                data=history_data,
                table_name=table_name
            )

            if 'status' in predictions and predictions['status'] == 'error':
                raise ValueError(f"预测失败: {predictions['message']}")

            predicted_values = list(predictions['predictions'].values())

            # 7. 获取真实值
            actual_data = full_data.iloc[base_date_idx + 1: base_date_idx + test_days + 1]
            actual_values = actual_data[self.target_col].tolist()

            # 确保预测和真实值长度一致
            min_length = min(len(predicted_values), len(actual_values))
            predicted_values = predicted_values[:min_length]
            actual_values = actual_values[:min_length]

            # 8. 计算评估指标
            metrics = {
                'MAE': round(mean_absolute_error(actual_values, predicted_values), 4),
                'RMSE': round(np.sqrt(mean_squared_error(actual_values, predicted_values)), 4),
                'R2': round(r2_score(actual_values, predicted_values), 4),
                'test_days': test_days,
                'base_date': str(base_date)[:10],
                'model_horizon': horizon
            }

            # 9. 生成评估图表
            dates = actual_data.index
            plot_path = self._save_multioutput_evaluation_plot(
                dates=dates,
                actual=actual_values,
                predicted=predicted_values,
                title=f'MultiOutput模型_{test_days}天真实预测对比图',
                metrics=metrics
            )

            # 10. 打印评估结果
            print("\n多输出模型评估结果:")
            print(f"基准日: {metrics['base_date']}")
            for k, v in metrics.items():
                if k not in ['test_days', 'base_date', 'model_horizon']:
                    print(f"{k}: {v:.4f}")

            return {
                'status': 'success',
                'metrics': metrics,
                'plot_path': plot_path,
                'predictions': list(zip(dates, predicted_values, actual_values))
            }

        except Exception as e:
            print(f"评估错误: {traceback.format_exc()}")
            return {
                'status': 'error',
                'message': str(e)
            }

    def _save_multioutput_evaluation_plot(self, dates, actual, predicted, title, metrics):
        """保存评估图表"""
        # 将列表转换为NumPy数组
        actual = np.array(actual)
        predicted = np.array(predicted)

        plt.figure(figsize=(15, 7))

        # 绘制实际值和预测值曲线
        plt.plot(dates, actual, 'b-', label='实际温度', linewidth=2)
        plt.plot(dates, predicted, 'r--', label='预测温度', linewidth=2)

        # 标记最大误差点（保留2位小数）
        errors = np.abs(actual - predicted)
        max_error_idx = np.argmax(errors)
        max_error = round(errors[max_error_idx], 2)
        plt.scatter(dates[max_error_idx], actual[max_error_idx],
                    color='green', s=100, label=f'最大误差点: {max_error}°C')

        # 添加评估指标文本
        metrics_text = (
            f"MAE: {metrics['MAE']}\n"
            f"RMSE: {metrics['RMSE']}\n"
            f"R²: {metrics['R2']}"
        )
        plt.text(0.02, 0.95, metrics_text, transform=plt.gca().transAxes,
                 bbox=dict(facecolor='white', alpha=0.8))

        # 图表装饰
        plt.title(title, fontsize=14)
        plt.xlabel('日期', fontsize=12)
        plt.ylabel('温度 (°C)', fontsize=12)
        plt.legend(fontsize=12, loc='upper right')
        plt.grid(True, linestyle='--', alpha=0.5)
        plt.xticks(rotation=45)
        plt.tight_layout()

        # 保存图像
        plot_dir = os.path.join('app', 'static', 'evaluation_plots')
        os.makedirs(plot_dir, exist_ok=True)
        filename = f'{self.target_col}_{title}.png'
        save_path = os.path.join(plot_dir, filename)
        plt.savefig(save_path, dpi=300, bbox_inches='tight')
        plt.close()

        print(f"评估图表已保存到: {save_path}")
        return save_path

if __name__ == '__main__':
    forecaster = TemperatureForecaster('guilin_max')
    result = forecaster.train_multioutput_model(horizon=90)
    print(result)

    predictions = forecaster.predict_multioutput(horizon=90)
    print(predictions)

    # forecaster = TemperatureForecaster('guilin_max')
    result = forecaster.evaluate_multioutput_model(test_days=90)
    print(result)
